package com.xiaocai.objview;

import android.Manifest;
import android.content.Intent;
import android.net.Uri;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class MainActivity extends AppCompatActivity implements GLSurfaceView.Renderer,
        View.OnTouchListener, ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnGestureListener,
        ActivityResultCallback<Uri> {
    private static final String TAG = "MainActivity";
    private GLSurfaceView glSurfaceView;

    private ObjView objView;

    private ObjViewShaderProgram objViewShaderProgram;

    private float[] projectionMatrix = new float[16];

    private float[] viewMatrix = new float[16];

    private float[] mvpMatrix = new float[16];

    private float[] modelMatrix = new float[16];

    private float[] temp = new float[16];

    private float angleX, angleY;

    private ObjLoad objLoad;

    private int width, height;

    private ScaleGestureDetector scaleGestureDetector;

    private GestureDetector gestureDetector;

    private float scaleXYZ = 1.0f;

    private float tempScaleXYZ = 1.0f;

    private LoadingCubeShaderProgram loadingCubeShaderProgram;

    private LoadingCube loadingCube;

    private float loadingAngleX;

    private Handler readHandler;

    private ActivityResultLauncher<String> resultLauncher;

    private ActivityResultLauncher<String[]> permissionLauncher;

    private ActivityResultLauncher<Intent> intentLauncher;

    private ActivityResultCallback<Map<String, Boolean>> permissionCallback = result -> {
        boolean p = true;
        for (Boolean value : result.values()) {
            p &= value;
        }
        if (p) {
            resultLauncher.launch("application/octet-stream");
        }
    };

    private ActivityResultCallback<ActivityResult> resultCallback = result -> {

    };

    private long startGLTime;

    private int fps;

    private TextView fpsView;

    private FpsUpdate fpsUpdate;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        resultLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), this);
        permissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionCallback);
        intentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), resultCallback);
        fpsView = findViewById(R.id.fps_view);
        glSurfaceView = findViewById(R.id.gl_surface);
        glSurfaceView.setOnTouchListener(this);
        glSurfaceView.setEGLContextClientVersion(2);
        glSurfaceView.setRenderer(this);
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
        objLoad = new ObjLoad(this);
        fpsUpdate = new FpsUpdate(this);

        scaleGestureDetector = new ScaleGestureDetector(this, this);
        gestureDetector = new GestureDetector(this, this);

        HandlerThread handlerThread = new HandlerThread("read-thread");
        handlerThread.start();
        readHandler = new Handler(handlerThread.getLooper());
        readHandler.postDelayed(new ReadObj("assets", "indoor plant_02.obj", this), 1000);
    }

    @Override
    protected void onResume() {
        super.onResume();
        glSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        glSurfaceView.onPause();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        objLoad.stop();
        readHandler.getLooper().quit();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.item_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        int itemId = item.getItemId();
        if (itemId == R.id.file) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
                intentLauncher.launch(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION));
                return super.onOptionsItemSelected(item);
            }

            List<String> list = new ArrayList<>();
            list.add(Manifest.permission.READ_EXTERNAL_STORAGE);
            permissionLauncher.launch(list.toArray(new String[0]));
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        startGLTime = 0;
        fps = 0;
        GLES20.glClearColor(1, 1, 1, 1);
        //打开深度测试
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);

        objViewShaderProgram = new ObjViewShaderProgram(this);

        loadingCube = new LoadingCube();
        loadingCubeShaderProgram = new LoadingCubeShaderProgram(this);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        this.width = width;
        this.height = height;
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        if (startGLTime == 0) {
            startGLTime = SystemClock.elapsedRealtime();
        }

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        if (objView != null) {
            Arrays.fill(temp, 0);
            Matrix.setIdentityM(modelMatrix, 0);
            Matrix.rotateM(modelMatrix, 0, angleY, 1, 0, 0);
            Matrix.rotateM(modelMatrix, 0, angleX, 0, 1, 0);
            Matrix.translateM(modelMatrix, 0, -(objLoad.getMaxX() + objLoad.getMinX()) / 2f * scaleXYZ,
                    -(objLoad.getMaxY() + objLoad.getMinY()) / 2f * scaleXYZ,
                    -(objLoad.getMaxZ() + objLoad.getMinZ()) / 2f * scaleXYZ);
            Matrix.scaleM(modelMatrix, 0, scaleXYZ, scaleXYZ, scaleXYZ);
            Matrix.setLookAtM(viewMatrix, 0, 0, 0, (objLoad.getMaxZ() == 0 ? 2 : objLoad.getMaxZ()) * 18,
                    0, 0, 0,
                    0, 1, 0);
            Matrix.perspectiveM(projectionMatrix, 0, 45, width / (float) height, (objLoad.getMaxZ() == 0 ? 2 : objLoad.getMaxZ()) * 2, objLoad.getZLength() * 20);
            Arrays.fill(temp, 0);
            Matrix.multiplyMM(temp, 0, projectionMatrix, 0, viewMatrix, 0);
            Matrix.multiplyMM(mvpMatrix, 0, temp, 0, modelMatrix, 0);

            objViewShaderProgram.useProgram();
//            objViewShaderProgram.setUniforms(mvpMatrix);

            objView.setWidthAndHeight(width, height);
            objView.draw(objViewShaderProgram, mvpMatrix);
        } else {

            Arrays.fill(temp, 0);
            Matrix.setIdentityM(modelMatrix, 0);
            Matrix.rotateM(modelMatrix, 0, loadingAngleX, 0, 1, 0);

            loadingAngleX -= 3;
            if (loadingAngleX < -360) {
                loadingAngleX = 0;
            }
            Matrix.setLookAtM(viewMatrix, 0, 15, 15, 25,
                    0, 0, 0, 0, 1, 0);
            Matrix.perspectiveM(projectionMatrix, 0, 45, width / (float) height, 2, 50);
            Arrays.fill(temp, 0);
            Matrix.multiplyMM(temp, 0, projectionMatrix, 0, viewMatrix, 0);
            Matrix.multiplyMM(mvpMatrix, 0, temp, 0, modelMatrix, 0);

            loadingCubeShaderProgram.useProgram();
            loadingCubeShaderProgram.setUniforms(mvpMatrix);

            GLES20.glDepthMask(false);
            GLES20.glEnable(GLES20.GL_BLEND);
            GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);

            loadingCube.bindData(loadingCubeShaderProgram);
            loadingCube.draw();

            GLES20.glDisable(GLES20.GL_BLEND);
            GLES20.glDepthMask(true);
        }

        fps++;
        long realtime = SystemClock.elapsedRealtime();
        long elapsed = realtime - startGLTime;
        if (elapsed >= 1000) {
            Log.d(TAG, "fps:" + fps);
            fpsUpdate.setFps(fps);
            fpsView.post(fpsUpdate);
            fps = 0;
            startGLTime = realtime;
        }
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        scaleGestureDetector.onTouchEvent(event);
        gestureDetector.onTouchEvent(event);
        return true;
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        scaleXYZ = tempScaleXYZ * detector.getScaleFactor();
        return false;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        if (objView == null) {
            return false;
        }

        tempScaleXYZ = scaleXYZ;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (objView == null) {
            return false;
        }

        angleX -= (distanceX / 8f);
        angleY -= (distanceY / 8f);

        if (angleY > 90) {
            angleY = 90;
        }
        if (angleY < -90) {
            angleY = -90;
        }
        return true;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public void onActivityResult(Uri result) {
        if (result == null) {
            return;
        }

        String path = AndroidUtils.getPathFromUri(this, result);
        Log.d(TAG, "onActivityResult: " + path);

        if (path != null) {
            File file = new File(path);
            if (!file.getName().toLowerCase().endsWith("obj")) {
                Toast.makeText(this, R.string.only_support_obj, Toast.LENGTH_SHORT).show();
                return;
            }

            objLoad.stop();
            objView = null;
            angleX = 0;
            angleY = 0;
            scaleXYZ = 1;

            readHandler.removeCallbacksAndMessages(null);
            readHandler.post(new ReadObj(file.getParent(), file.getName(), this));
        }
    }

    private static class ReadObj implements Runnable {

        private String parent;

        private String name;

        private WeakReference<MainActivity> activity;

        public ReadObj(String parent, String name, MainActivity activity) {
            this.parent = parent;
            this.name = name;
            this.activity = new WeakReference<>(activity);
        }

        @Override
        public void run() {
            boolean ok = activity.get().objLoad.readObjFile(name, parent);
            if (ok) {
                activity.get().objView = new ObjView(activity.get().objLoad);
            }
        }
    }

    private static class FpsUpdate implements Runnable {

        private int fps;

        private WeakReference<MainActivity> activity;

        public FpsUpdate(MainActivity activity) {
            this.activity = new WeakReference<>(activity);
        }

        public void setFps(int fps) {
            this.fps = fps;
        }

        @Override
        public void run() {
            activity.get().fpsView.setText(String.valueOf(fps));
        }
    }
}